為什麼要做測試? 今天在外面餐廳吃飯,廚師在出菜前會先試吃看看味道對不對;一台咖啡機,在出廠前也會經過一番測試,確認出水是否有異常? 壓力表測壓準不準,加熱模組有沒有正常啟動,這些測試都沒問題,才會賣到消費者手上。對我們程式設計師來說,我們開發的應用程式就是給使用者的產品,因此在給人使用前,當然也要經過測試。
應用程式在開發初期規模還不大的時候,要驗證程式是否如預期,通常都用人工測試的方式來驗證。
但隨著應用程式越來越龐大,人工測試所耗費的時間越來越多,這個時候可以試著導入自動化測試,自動化測試的第一步就是單元測試,單元測試帶來的幾個好處:
說了這麼多單元測試的優點,我們來看看Blazor要怎麼進行單元測試吧
目前微軟官方尚未有自己的Blazor測試框架,較知名的是社群開發的bUnit。
接下來我們會使用bUnit來測試Blazor專案預設的幾個元件
建立好Blazor專案,Server或WebAssembly都可以,再建立一個測試專案。因為bUnit本身需要一個測試框架來執行測試案例,因此建立時可以選一個習慣用的測試框架專案,這邊我較熟悉Nunit,所以建立NUnit測試專案(.net Core)
在測試專案安裝bUnit。
記得要勾選包括搶鮮版,才會顯示bunit。如果剛剛選擇的是xUnit測試專案,安裝第一個bunit就可以了,如果是MSTest或NUnit,就裝bunit.web和bunit.core
安裝好後,加入專案參考
接下來可以撰寫測試程式了,一開始先來測試最單純的index元件
[Test]
public void IndexShouldRender()
{
var ctx = new Bunit.TestContext();
//cut = component under test
var cut = ctx.RenderComponent<BlazorUITest.Pages.Index>();
cut.MarkupMatches("<h1>Hello, world!</h1>");
}
通過第一個測試囉~
再來測試Counter元件,Counter元件中每按一下button,p標籤內的數字會加1,因此我們準備來測試這個行為
[Test]
public void CounterShouldIncrementWhenSelected()
{
var ctx = new Bunit.TestContext();
//Arrange
var cut = ctx.RenderComponent<Counter>();
var element = cut.Find("p");
//Act
cut.Find("button").Click();
string elementText = element.TextContent;
//Assert
elementText.MarkupMatches("Current count: 1");
}
Counter也通過測試囉
測試FetchData元件
在FetchData中,初始化時會透過HttpClient取得json資料,但因為HttpClient不是介面所以不容易mock,因此我們另外建立一個WeatherService和介面IWeatherService,將OnInitializedAsync中的Http.GetFromJsonAsync搬到WeatherService內:
public class WeatherService : IWeatherService
{
public async Task<WeatherForecast[]> GetWeatherDataAsync()
{
HttpClient httpClient = new HttpClient();
httpClient.BaseAddress = new Uri("http://localhost:56692/");
WeatherForecast[] data = await httpClient.GetFromJsonAsync<WeatherForecast[]>("sample-data/weather.json");
return data;
}
}
在programs.cs註冊IWeatherService:
builder.Services.AddScoped<IWeatherService, WeatherService>();
原本FetchData注入HttpClient,改成注入IWeatherService:
@page "/fetchdata"
@inject IWeatherService weatherService
<h1>Weather forecast</h1>
<p>This component demonstrates fetching data from the server.</p>
//略...
@code {
private WeatherForecast[] forecasts;
protected override async Task OnInitializedAsync()
{
forecasts = await weatherService.GetWeatherDataAsync();
}
public class WeatherForecast
{
public DateTime Date { get; set; }
public int TemperatureC { get; set; }
public string Summary { get; set; }
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}
}
確認FetchData可以正常執行
FetchData有2個情境要測式:
這邊使用NSubStitute mocking library,來mock剛剛建立的IWeatherService
第一個情境,沒有資料時,顯示loading
[Test]
public void FetchDataShouldRenderLoadingWhenDataIsNull()
{
var ctx = new Bunit.TestContext();
//mock WeatherService
var mockService = Substitute.For<IWeatherService>();
//設定WeatherService的GetWeatherDataAsync方法回傳null
mockService.GetWeatherDataAsync().Returns(Task.FromResult<FetchData.WeatherForecast[]>(null));
//註冊到Services
ctx.Services.AddSingleton<IWeatherService>(mockService);
var cut = ctx.RenderComponent<FetchData>();
var expectedHtml = @"<h1>Weather forecast</h1>
<p>This component demonstrates fetching data from the server.</p>
<p><em>Loading...</em></p>";
cut.MarkupMatches(expectedHtml);
}
測試成功
接著測試第2個情境,有資料時用table顯示
[Test]
public void FetchDataShouldRenderLoadingWhenDataIsNotNull()
{
var ctx = new Bunit.TestContext();
var mockService = Substitute.For<IWeatherService>();
mockService.GetWeatherDataAsync().Returns(Task.FromResult(new WeatherForecast[] { new WeatherForecast() { TemperatureC = 30, Summary = "test", Date = new DateTime(2020, 10, 6) } }));
ctx.Services.AddSingleton<IWeatherService>(mockService);
var cut = ctx.RenderComponent<FetchData>();
var expectedHtml = @"<h1>Weather forecast</h1>
<p>This component demonstrates fetching data from the server.</p>
<table class='table'>
<thead>
<tr>
<th>Date</th>
<th>Temp. (C)</th>
<th>Temp. (F)</th>
<th>Summary</th>
</tr>
</thead>
<tbody>
<tr>
<td>2020/10/6</td>
<td>30</td>
<td>85</td>
<td>test</td>
</tr>
</tbody>
</table>";
cut.MarkupMatches(expectedHtml);
}
測試成功
程式碼可參考:https://github.com/CircleLin/BlazorUITest